﻿#region "Copyright (C) Lenny Granovsky. 2005-2013"
//This program is free software distribute under the terms of the GNU General Public License as published by
//the Free Software Foundation. Please read AssemblyInfo.cs file for more information.
#endregion

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.Routing;
using System.Reflection;

namespace Web.Enhancements.Rest
{
    public static class HttpRequestExtension
    {
        /// <summary>
        /// Method checks whether current request recieved and being processed by WebRest services framework as RESTfull service.
        /// </summary>
        /// <remarks>The outcome of this method is only valid after Request Handler has been assigned to the request.</remarks>
        /// <returns>Return boolean value: true if RESTful service, otherwise false.</returns>
        public static bool IsRestfulService(this System.Web.HttpRequest request)
        {
            return (Web.Enhancements.Rest.WebRestServiceContext.Current != null);
        }
    }

    public class WebRestClient
    {
        public const string DefaultCallbackFunctionParameterName = "data";

        /// <summary>
        /// Returns a site relative HTTP path from a partial path starting out with a ~.
        /// Same syntax that ASP.Net internally supports but this method can be used
        /// outside of the Page framework.
        /// 
        /// Works like Control.ResolveUrl including support for ~ syntax
        /// but returns an absolute URL.
        /// </summary>
        /// <param name="originalUrl">Any Url including those starting with ~</param>
        /// <returns>relative url</returns>
        private static string ResolveUrl(string originalUrl)
        {    
            if (originalUrl == null)        
                return null;     
            // *** Absolute path - just return    
            if (originalUrl.IndexOf("://") != -1)        
                return originalUrl;     
            // *** Fix up image path for ~ root app dir directory    
            if (originalUrl.StartsWith("~"))    
            {        
                string newUrl = "";        
                if (System.Web.HttpContext.Current != null)            
                    newUrl = System.Web.HttpContext.Current.Request.ApplicationPath +                  
                        originalUrl.Substring(1).Replace("//", "/");        
                else            
                    // *** Not context: assume current directory is the base directory            
                    throw new ArgumentException("Invalid URL: Relative URL not allowed.");   
                // *** Just to be sure fix up any double slashes
                if (newUrl.StartsWith("//"))
                    return newUrl.Replace("//", "/");
                return newUrl;
            }     
            return originalUrl;
        }
        
        /// <summary>
        /// Method builds URL that can be use from the Client to call specific method in a class with parameters.
        /// </summary>
        /// <param name="type">Type to call.</param>
        /// <param name="methodName">Method to call.</param>
        /// <param name="parameters">List of parameters (optional).</param>
        /// <returns>Returns URL.</returns>
        public static string GetUrlFunctionCall(Type type, string methodName, params object[] parameters)
        {
            System.Reflection.MethodInfo mInfo = type.GetMethod(methodName);
            ParameterUrlFormatter format = ParameterUrlFormatter.CreateInstance(mInfo);
            string urlParams = format.FormatParameters(parameters);
            return GetUrlFunctionCall2(type, methodName, urlParams);
        }
        public static string GetUrlFunctionCall<T>(string methodName, params object[] parameters)
        {
            return GetUrlFunctionCall(typeof(T), methodName, parameters);
        }

        /// <summary>
        /// Method builds URL that can be use from the Client to call specific method in a class with parameters.
        /// </summary>
        /// <remarks>This method is for internal use only.</remarks>
        /// <param name="type">Type to call.</param>
        /// <param name="methodName">Method to call.</param>
        /// <param name="parameters">Parameters as name=value &-delimited list (QueryString style).</param>
        /// <returns>Returns URL.</returns>
        internal static string GetUrlFunctionCall2(Type type, string methodName, string parameters)
        {
            string fName = type.FullName;
            bool replaceDotWithPathSeparator = true;
            WebRestClassConfiguration TypeConfig = WebRestSettings.Current.TypeMappings.Find(type.FullName, type.Assembly.FullName);
            if (TypeConfig != null)
            {
                fName = TypeConfig.AliasName;
                replaceDotWithPathSeparator = TypeConfig.ReplaceDotWithPathSeparator;
            }
            return GetUrlFunctionCall2(fName, methodName, parameters, replaceDotWithPathSeparator);
        }
        internal static string GetUrlFunctionCall2(string translatedAliasTypeName, string methodName, string parameters, bool replaceDotWithPathSeparator)
        {
            if (replaceDotWithPathSeparator)
                translatedAliasTypeName = translatedAliasTypeName.Replace('.', '/');
            string url = "";
            if (parameters.Length > 0)
                url = string.Format("~/{0}.{1}?{2}&{3}", translatedAliasTypeName, WebRestSettings.Current.RestFileExtension, methodName, parameters);
            else
                url = string.Format("~/{0}.{1}?{2}", translatedAliasTypeName, WebRestSettings.Current.RestFileExtension, methodName);
            return ResolveUrl(url);
        }

        /// <summary>
        /// Method builds javascript that can be use from the Client to call specific method in a class with parameters.
        /// </summary>
        /// <param name="type">Type to call.</param>
        /// <param name="methodName">Method to call.</param>
        /// <param name="parameters">List of parameters (optional).</param>
        /// <returns>Returns URL.</returns>
        public static string GetJSClientFunctionCall(Type type, string methodName, params object[] parameters)
        {
            return string.Format("javascript:{0}", GetJSClientFunctionCallUnwrapped(type, methodName, parameters));
        }
        public static string GetJSClientFunctionCall<T>(string methodName, params object[] parameters)
        {
            return GetJSClientFunctionCall(typeof(T), methodName, parameters);
        }

        /// <summary>
        /// Method builds javascript that can be use from the Client to call specific method in a class with parameters without prefixing call with "javascript".
        /// </summary>
        /// <param name="type">Type to call.</param>
        /// <param name="methodName">Method to call.</param>
        /// <param name="parameters">List of parameters (optional).</param>
        /// <returns>Returns URL.</returns>
        public static string GetJSClientFunctionCallUnwrapped(Type type, string methodName, params object[] parameters)
        {
            string typeFullName = TypeMapper.TranslateFromType(type.FullName, type.Assembly.FullName);
            string pars = String.Empty;
            if (parameters.Length > 0)
            {
                ParameterJSFormatter format = ParameterJSFormatter.CreateInstance();
                pars = format.FormatParameters(parameters);
            }
            return string.Format("{0}.{1}({2});", typeFullName, methodName, pars);
        }
        public static string GetJSClientFunctionCallUnwrapped<T>(string methodName, params object[] parameters)
        {
            return GetJSClientFunctionCallUnwrapped(typeof(T), methodName, parameters);
        }

        /// <summary>
        /// Method builds javascript that can be use from the Client to call specific method in a class with parameters 
        /// and registers callback javascript.
        /// </summary>
        /// <param name="type"></param>
        /// <param name="methodName"></param>
        /// <param name="javascript"></param>
        /// <param name="parameters">List of parameters (optional).</param>
        /// <returns></returns>
        public static string GetJSClientFunctionCallWithCallback(Type type, string methodName, string javascript, params object[] parameters)
        {
            string rtn = GetJSClientFunctionCall(type, methodName, parameters);
            RegisterJSCallback(type, methodName, javascript);
            return rtn;
        }
        public static string GetJSClientFunctionCallWithCallback<T>(string methodName, string javascript, params object[] parameters)
        {
            return GetJSClientFunctionCallWithCallback(typeof(T), methodName, javascript, parameters);
        }

        /// <summary>
        /// Method builds javascript that can be use from the Client to call specific method in a class with parameters  without prefixing call with "javascript" 
        /// and registers callback javascript.
        /// </summary>
        /// <param name="type"></param>
        /// <param name="methodName"></param>
        /// <param name="javascript"></param>
        /// <param name="parameters">List of parameters (optional).</param>
        /// <returns></returns>
        public static string GetJSClientFunctionCallUnwrappedWithCallback(Type type, string methodName, string javascript, params object[] parameters)
        {
            string rtn = GetJSClientFunctionCallUnwrapped(type, methodName, parameters);
            RegisterJSCallback(type, methodName, javascript);
            return rtn;
        }
        public static string GetJSClientFunctionCallUnwrappedWithCallback<T>(string methodName, string javascript, params object[] parameters)
        {
            return GetJSClientFunctionCallUnwrappedWithCallback(typeof(T), methodName, javascript, parameters);
        }

        /// <summary>
        /// Method builds function name to use within client javascript by given type and method name.
        /// </summary>
        /// <param name="type"></param>
        /// <param name="methodName"></param>
        /// <returns></returns>
        public static string GetJSClientFunctionName(Type type, string methodName)
        {
            string typeFullName = TypeMapper.TranslateFromType(type.FullName, type.Assembly.FullName);
            return string.Format("{0}.{1}", typeFullName, methodName);
        }
        public static string GetJSClientFunctionName<T>(string methodName)
        {
            return GetJSClientFunctionName(typeof(T), methodName);
        }

        public static string GetJSClientDefaultCallbackName(Type type, string methodName)
        {
            string typeFullName = TypeMapper.TranslateFromType(type.FullName, type.Assembly.FullName);
            return string.Format("{0}.{1}Callback", typeFullName, methodName);
        }
        public static string GetJSClientDefaultCallbackName<T>(string methodName)
        {
            return GetJSClientDefaultCallbackName(typeof(T), methodName);
        }

        /// <summary>
        /// Method registers special javascript block for all class methods avilable for calling as rest service.
        /// </summary>
        /// <param name="type">Type to register.</param>
        /// <returns>Returns true if type was successfully registered; otherwise - false.</returns>
        public static bool RegisterJSClient(Type type)
        {
            if (System.Web.HttpContext.Current != null)
            {
                if (System.Web.HttpContext.Current.CurrentHandler != null)
                {
                    if (System.Web.HttpContext.Current.CurrentHandler is System.Web.UI.Page)
                    {
                        System.Web.UI.Page page = (System.Web.UI.Page)System.Web.HttpContext.Current.CurrentHandler;
                        AssureWebRestExplorerReferenceIsRegistered(ref page);
                        RegisterClassReference(ref page, type);
                        return true;
                    }
                }
            }
            return false;
        }

        /// <summary>
        /// Methid registers the WebRestExplorer client JS if it's not registered yet.
        /// </summary>
        /// <param name="page">Reference to the current page.</param>
        private static void AssureWebRestExplorerReferenceIsRegistered(ref System.Web.UI.Page page)
        {
            if (!page.ClientScript.IsClientScriptIncludeRegistered("Rest.RestClientJSDebug.js"))
            {
                string script = string.Format("var webRestSID='{0}';", Guid.NewGuid().ToString());
#if DEBUG
                string rsname = "Web.Enhancements.Rest.RestClientJSDebug.js";
#else
                string rsname = "Web.Enhancements.Rest.RestClientJSRelease.js";
#endif
                Type rstype = typeof(WebRestExplorer);
                string url = page.ClientScript.GetWebResourceUrl(rstype, rsname);
                page.ClientScript.RegisterClientScriptResource(rstype, rsname);
                page.ClientScript.RegisterClientScriptBlock(page.GetType(), "Rest.RestClientJSDebug_SID", script, true);
            }
        }

        /// <summary>
        /// Methid registers the class JS reference.
        /// </summary>
        /// <param name="page">Reference to the current page.</param>
        /// <param name="type">Type to register.</param>
        private static void RegisterClassReference(ref System.Web.UI.Page page, Type type)
        {
            if (!page.ClientScript.IsClientScriptIncludeRegistered(type.FullName))
            {
                string assemblyQualifiedName = Convert.ToBase64String(System.Text.Encoding.Default.GetBytes(type.AssemblyQualifiedName));
                page.ClientScript.RegisterClientScriptInclude(type.FullName, GetUrlFunctionCall2(typeof(WebRestExplorer), "RenderClass", string.Format("typeName={0}", assemblyQualifiedName)));
            }
        }

        /// <summary>
        /// Method registers javascript callback block (on current page) for specified class and method with provided custom javascript code.
        /// </summary>
        /// <param name="type"></param>
        /// <param name="method"></param>
        /// <param name="javascript"></param>
        /// <returns></returns>
        public static bool RegisterJSCallback(Type type, string method, string javascript)
        {
            if (System.Web.HttpContext.Current != null)
            {
                if (System.Web.HttpContext.Current.CurrentHandler != null)
                {
                    if (System.Web.HttpContext.Current.CurrentHandler is System.Web.UI.Page)
                    {
                        System.Web.UI.Page page = (System.Web.UI.Page)System.Web.HttpContext.Current.CurrentHandler;
                        string scriptName = string.Format("{0}{1}CallbackScript", type.FullName.Replace(".", ""), method);
                        if (!page.ClientScript.IsClientScriptBlockRegistered(page.GetType(), scriptName))
                        {
                            string fullCallbackFunctionName = WebRestClient.GetJSClientDefaultCallbackName(type, method);
                            string shortCallbackFunctionName = fullCallbackFunctionName.Substring(fullCallbackFunctionName.LastIndexOf('.') + 1);
                            string script = string.Format("{0} = function {1}({2}){{ {3} }}; \n", fullCallbackFunctionName, shortCallbackFunctionName, DefaultCallbackFunctionParameterName, javascript);
                            page.ClientScript.RegisterClientScriptBlock(page.GetType(), scriptName, script, true);
                            return true;
                        }
                    }
                }
            }
            return false;
        }
        public static bool RegisterJSCallback<T>(string method, string javascript)
        {
            return RegisterJSCallback(typeof(T), method, javascript);
        }

        public static bool RegisterJSCallback(Type type, string method, Templates.JavaScriptTemplateType javascriptTemplate)
        {
            string script = Templates.JavaScriptTemplateGenerator.GenerateTemplate(javascriptTemplate);
            return RegisterJSCallback(type, method, script);
        }
        public static bool RegisterJSCallback<T>(string method, Templates.JavaScriptTemplateType javascriptTemplate)
        {
            return RegisterJSCallback(typeof(T), method, javascriptTemplate);
        }

        public static bool RegisterJSCallback(Type type, string method, Templates.JavaScriptTemplateType javascriptTemplate, string templateParameter)
        {
            string script = Templates.JavaScriptTemplateGenerator.GenerateTemplate(javascriptTemplate, templateParameter);
            return RegisterJSCallback(type, method, script);
        }
        public static bool RegisterJSCallback<T>(string method, Templates.JavaScriptTemplateType javascriptTemplate, string templateParameter)
        {
            return RegisterJSCallback(typeof(T), method, javascriptTemplate, templateParameter);
        }

        /// <summary>
        /// Method registers routes for the class type.
        /// </summary>
        /// <param name="type">Type to register.</param>
        public static void RegisterTypeRoutes(Type type)
        {
            if (type.GetInterface(typeof(IWebRestService).FullName) == null)
                throw new WebRestException("Submitted type of " + type.FullName + " is not REST Service, therefore routes cannot be registered.");

            char[] httpMethodDivider = new char[] { ',' };
            object[] tmp1 = type.GetCustomAttributes(typeof(Web.Enhancements.Rest.RegisterRouteAttribute), false);
            Web.Enhancements.Rest.RegisterRouteAttribute routeAttr = null;
            for (int ii = 0; ii < tmp1.Length; ii++)
            {
                routeAttr = (Web.Enhancements.Rest.RegisterRouteAttribute)tmp1[ii];
                string routeName = string.Format("WebRestRoute-{0}-{1}", type.FullName, ii.ToString());
                if (RouteTable.Routes[routeName] == null)
                {
                    RouteValueDictionary dict = new RouteValueDictionary();
                    dict.Add("namespace", type.Namespace);
                    dict.Add("class", type.Name);
                    if (!string.IsNullOrWhiteSpace(routeAttr.BindToMethod))
                        dict.Add("method", routeAttr.BindToMethod);
                    if (routeAttr.LimitByHttpMethods.Length > 0)
                    {
                        HttpMethodConstraint methodConstraints = new HttpMethodConstraint(routeAttr.LimitByHttpMethods.Split(httpMethodDivider));
                        RouteTable.Routes.Add(routeName, new Route(routeAttr.Route, dict, new RouteValueDictionary { { "httpMethod", methodConstraints } }, new WebRestRouteHandler()));
                    }
                    else
                        RouteTable.Routes.Add(routeName, new Route(routeAttr.Route, dict, new WebRestRouteHandler()));
                }
            }

            MethodInfo[] mis = type.GetMethods(BindingFlags.IgnoreCase | BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy);
            object[] tmp = null;
            foreach (MethodInfo mi in mis)
            {
                tmp = mi.GetCustomAttributes(typeof(Web.Enhancements.Rest.RegisterRouteAttribute), false);
                for (int ii = 0; ii < tmp.Length; ii++)
                {
                    routeAttr = (Web.Enhancements.Rest.RegisterRouteAttribute)tmp[ii];
                    string routeName = string.Format("WebRestRoute-{0}.{1}-{2}", type.FullName, mi.Name, ii.ToString());
                    if (RouteTable.Routes[routeName] == null)
                    {
                        RouteValueDictionary dict = new RouteValueDictionary();
                        dict.Add("namespace", type.Namespace);
                        dict.Add("class", type.Name);
                        dict.Add("method", mi.Name);
                        if (routeAttr.LimitByHttpMethods.Length > 0)
                        {
                            HttpMethodConstraint methodConstraints = new HttpMethodConstraint(routeAttr.LimitByHttpMethods.Split(httpMethodDivider));
                            RouteTable.Routes.Add(routeName, new Route(routeAttr.Route, dict, new RouteValueDictionary { { "httpMethod", methodConstraints } }, new WebRestRouteHandler()));
                        }
                        else
                            RouteTable.Routes.Add(routeName, new Route(routeAttr.Route, dict, new WebRestRouteHandler()));
                    }
                }
            }
        }
    }
}
